iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0


這天,小櫻在捕捉Golang牌時,Golang牌被嚇到,並且受傷,好巧不巧被一個田徑隊的學姐抓去收養。因為學姐跑太慢,所以好心的Golang牌決定開外掛讓學姐跑快一點,像這樣:

就好比演算法作業跑太慢的時候沒關係,多開幾個執行緒下去給他就好了(誤)

究竟學姐有沒有跑贏比賽呢?我才不會爆雷呢!要看自己去看

利用 select 同時監聽多個 channel

package main
import "fmt"
func main(){
    ch1 := make(chan int)
    ch2 := make(chan int)
    go func(){
        ch1 <- 1
    }()

    go func(){
        ch2 <- 2
    }()

    select{
    case <- ch1:
        fmt.Println("接收ch1")
    case <- ch2:
        fmt.Println("接收ch2")
    }
}

執行結果:
接收ch2

每次執行都不一樣,由作業系統決定

利用 select + case 可以同時監聽不同的 channel 與 switch 不一樣的地方在於 switch 是由上到下有順序的比對,而 select 是同時比對、隨機的

利用 select 監聽同一個 channel

如果有多個 case 同時收到訊息,則 select 會隨機挑選 case:

package main
import "fmt"
func main(){
    for i:=0; i<10; i=i+1{
        ch := make(chan int)    
        go func(){
            ch <- 1
        }()

        select{ // 三個 case 會同時收到 ch 上的訊息
        case <- ch:
            fmt.Println("1")
        case <- ch:
            fmt.Println("2")
        case <- ch:
            fmt.Println("3")
        }
    }
}

執行結果:
1
2
3
3
3
3
2
1
3
2

每次執行都不一樣,由作業系統決定

設置計時器,若超時就不繼續監聽

select 在實際使用上常常搭配計時器使用,當時間超過時就會結束監聽。另開一個通道做為計時器使用,時間一到就會發送訊息將 select 的等待打斷

package main
import(
    "fmt"
    "time"
)
func main(){
    ch := make(chan int)
    timeout := make(chan bool)

    // 檢查有沒有超過 3 秒
    go func(){
        time.Sleep(3 * time.Second)
        timeout <- true
    }()

    // 模擬一個需要耗時五秒的執行緒
    go func(){
        // 可自行調整秒數試試
        time.Sleep(5 * time.Second)
        ch <- 10
    }()

    select{
    case num := <- ch:
        fmt.Println(num)
    case <- timeout:
        fmt.Println("Time out !!")
    }
}

執行結果:
Time out !!

在示範中,因為在收到 ch 訊息前就先收到 timeout 的訊息了,所以主執行緒離開 select,不會被擋住。這可以用來確保 select 最多只會給予 3 秒的等待。

更簡單地設置計時器

圖片來源:庫洛魔法使第一季第卅五集

因為計時器常常使用,但使用時還要考慮計時的起點還有通道的使用等,還挺麻煩的,因此在 time package 中已經預設了一個函式,可以快速啟用計時器

func After(d Duration) <-chan Time

After waits for the duration to elapse and then sends the current time on the returned channel. It is equivalent to NewTimer(d).C. The underlying Timer is not recovered by the garbage collector until the timer fires. If efficiency is a concern, use NewTimer instead and call Timer.Stop if the timer is no longer needed.

package main
import(
    "fmt"
    "time"
)
func main(){
    ch := make(chan int)

    // 模擬一個需要耗時五秒的執行緒
    go func(){
        // 可自行調整秒數試試
        time.Sleep(5 * time.Second)
        ch <- 10
    }()

    select{
    case num := <- ch:
        fmt.Println(num)
    case timer := <- time.After(3 * time.Second):
        fmt.Println("Time out !!")
        fmt.Println(timer)
    }
}

執行結果
Time out !!
2020-09-19 18:17:13.878545989 +0000 UTC m=+3.000823994

執行結果第二行與作業系統當前時間有關

設置 default 使 select 不做等待

當 channel 沒有值傳送,select 會一直等待,此時變化造成 deadlock,如果有設置 default 則 select 不會做等待,注意!是「完全不會等待」

以下示範在沒有 default 的情況下,select 做無止盡的等待:

package main
import "fmt"
func main(){
    ch := make(chan int)
    select{
    case <- ch:
        fmt.Println("1")
    }
}

執行結果:
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
/mnt/588F-A4C6/go-tutorial/lesson19a.go:6 +0x56
exit status 2

使用 default:

package main
import "fmt"
func main(){
    ch := make(chan int)
    select{
    case <- ch:
        fmt.Println("1")
    default:
        fmt.Println("exit")
    }
}

執行結果:
exit

default 不會針測是否 deadblock,而是沒在等的

如果有設置 ch 傳送訊息但比較慢一點點才收信那麼 select 會怎麼選擇呢? select 會直接選 default 因為有 default 在的情況下,select 不會等待其他 case

package main
import(
    "fmt"
    "time"
)

func main(){
    ch := make(chan int)
    go func(){
        time.Sleep(1 * time.Second)
        ch <- 1
    }()

    select{
    case <- ch:
        fmt.Println("1")
    default:
        fmt.Println("exit")
    }
}

執行結果:
exit

換句話說在有設置 default 的情況下,select 不會做等待

注意以下情況

在有其他 case 的通道抵達的情況下,還會執行 defualt 內的動作嗎?

package main
import(
    "fmt"
    "time"
)

func main(){
    case1 := 0
    case2 := 0
    defaultcase := 0
    for i:=0; i < 10; i++{
        ch1 := make(chan bool)
        ch2 := make(chan bool)
        go func(){
            ch1 <- true
        }()

        go func(){
            ch2 <- true
        }()

        // 稍微等待一下確定 ch1, ch2 訊息都抵達
        time.Sleep(1 * time.Second)

        select{
        case <- ch1:
            case1 = case1 + 1
        case <- ch2:
            case2 = case2 + 1
        default:
            defaultcase = defaultcase + 1
        }
    }
    fmt.Println("case1", case1)
    fmt.Println("case2", case2)
    fmt.Println("default", defaultcase)
}

執行結果:
case1 5
case2 5
default 0

我試了幾次,case1, case2 幾乎都一半一半,不會跟 default 做競爭,基本上,在有其他 case 選擇的情況下,是不會選擇 default 的。只有在每個 case 都要等待時才會執行 default

利用 select default 的特性來感知 buffered channel 是否已滿

剛剛所使用的特性都是 unbuffered channel 在等待接收的特性。除了等待接收,buffered channel 在 channel 無空位的情況下也會做等待。利用這個等待的特性,搭配 default 可以用來檢查 buffered channel 是否已滿

package main
import "fmt"
func main(){
    buffer_channel := make(chan bool, 1)

    // 占滿 buffer_channel
    buffer_channel <- true

    select{
    case buffer_channel <- true:    // 嘗試傳值給 buffer_channel
        fmt.Println("buffer_channel 沒有滿")
    default:

        fmt.Println("buffer_channel 已滿")
    }
}

執行結果:
buffer_channel 已滿


後記

在使用 select 時:

  1. 每個 case 都會被執行,直到有一個 case 的等待被打破為止
  2. 如果同時有多個 case 的等待都被打破,那麼會隨機挑選一個執行
  3. default 可視為一個不用等待就已經被打破的 case,但是如果在其他 case 都被打破的情況下,default 不會被選擇

本文大多數圖片來自:


上一篇
#18 同步、異步與互斥鎖 ── sync 套件實戰 | Golang魔法使
下一篇
#20 單向通道、位元運算子、const 實現枚舉 小狼小可交換身體啦!| Golang魔法使
系列文
Golang魔法使 ─ 30天從零開始學習Go語言 | 比Python還簡單 | 理科生一定學得會 | 文科生不好說30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言